

package com.timber.util;

import java.util.*;

/**
 * This class allows to keep and synchronize against the database SLDataObject
 * objects. Also it caches simple selects from the database.
 */
public class StructuredCache {
    /**
     * Default initial capacity of the cache
     */
    public final static int DEFAULT_INITIALI_SIZE = 5000;

    private boolean loaded = false;
    protected final LinkedHashMap objects;
    protected final HashMap mappings;
    protected final HashSet removed;

    /**
     * Creates a cache of the specified size.
     */
    public StructuredCache(int initialCacheSize) {
        objects = new LinkedHashMap(initialCacheSize);
        removed = new HashSet();
        mappings = new HashMap();
        loaded = false;
        init();
    }

    /**
     * Creates a cache with default initial capacity.
     */
    public StructuredCache() {
        this(DEFAULT_INITIALI_SIZE);
    }

    /**
     * Clear's up the cached data.
     */
    public void clear() {
        objects.clear();
        removed.clear();
        mappings.clear();
        loaded = false;
    }

    /**
     * Clear's up only those items, that are linked to the key
     * specified. This method doesn't affect to the contents of the
     * "Select All" cache.
     */
    public void clearMapping(Object mapKey) {
        if (mapKey == SelectAll.SELECT_ALL) {
            clear();
        } else {
            mappings.remove(mapKey);
        }
    }

    /**
     * Loads all data objects during the initialization process.
     * This method doesn't replace existing items (with the same hashKey) and
     * doesn't load items that are marked as "removed".
     * @see #lookupAllObjects
     */
    private synchronized void loadAllObjects() throws Exception {
        if (!loaded) {
            Object dataSet = lookupAllObjects();
            if (dataSet != null) {
                if (dataSet instanceof List) {
                    for (Iterator i = ((List) dataSet).iterator(); i.hasNext();) {
                        Object obj = i.next();
                        Object objKey = ((SLDataObject) obj).hashKey();
                        if ((!objects.containsKey(objKey)) &&
                                (!removed.contains(objKey))) {
                            objects.put(objKey, obj);
                        }
                    }
                }
                loaded = true;
            } else {
                Util.error("ERROR! StructuredCache: lookupAllObjects shouldn't return NULL.");
            }
        }
    }

    /**
     * Loades objects mapped to the mapKey specified. This methods doesn't
     * reload items that already exist in "select all" cache.
     */
    private synchronized void loadMappedObjects(Object mapKey) throws Exception {
        if ((mapKey != SelectAll.SELECT_ALL) && (!mappings.containsKey(mapKey))) {
            Object dataSet = lookupMappedObjects(mapKey);
            if (dataSet != null) {
                if (dataSet instanceof List) {
                    List list = new ArrayList(Math.max(0,
                            ((List) dataSet).size() - removed.size()));
                    for (Iterator i = ((List) dataSet).iterator(); i.hasNext();) {
                        Object obj = i.next();
                        Object objKey = ((SLDataObject) obj).hashKey();
                        if (!removed.contains(objKey)) {
                            if (objects.containsKey(objKey)) {
                                obj = objects.get(objKey);
                            } else {
                                objects.put(objKey, obj);
                            }
                            list.add(obj);
                        }
                    }
                    mappings.put(mapKey, list);
                } else {
                    mappings.put(mapKey, new Nothing());
                }
            } else {
                Util.error("ERROR! StructuredCache: lookupMappedObjects shouldn't return NULL.");
            }
        }
    }

    /**
     * Puts a data object into the cache if it doesn't exists there.
     */
    public void putObject(SLDataObject dataObject) throws Exception {
        Object key = dataObject.hashKey();
        if (!objects.containsKey(key)) {
            objects.put(key, dataObject);
            removed.remove(key);
        } else {
            Util.error("ERROR! StructuredCache: Attempt to reassign object in cache.");
        }
    }

    /**
     * Retrieves an object by key from the cache.
     */
    public SLDataObject getObject(Object key) throws Exception {
        if (!objects.containsKey(key)) {
            loadAllObjects();
        }
        return (SLDataObject) objects.get(key);
    }

    /**
     * Checks the object with the key exists in the cache
     */
    public boolean hasObject(Object key) throws Exception {
        if (!objects.containsKey(key)) {
            loadAllObjects();
            return objects.containsKey(key);
        }
        return true;
    }

    /**
     * Returns all stored objects. This method executes lookupAllObjects
     * if the cache has not been loaded yet.
     * @see #lookupAllObjects
     */
    public List getAllObjects() throws Exception {
        loadAllObjects();
        return Collections.unmodifiableList(
                new ArrayList(objects.values()));
    }

    /**
     * Removes an object from the cache. The object gets removed from
     * all the mapped lists as well. This method implements
     * "transparent" removing of the items that have not been loaded yet.
     */
    public void removeObject(SLDataObject dataObject) throws Exception {
        Object key = dataObject.hashKey();
        Object obj = objects.remove(key);
        removed.add(key);
        if (obj != null) {
            for (Iterator i = mappings.values().iterator(); i.hasNext();) {
                Object o = i.next();
                if (o instanceof List) {
                    List list = (List) o;
                    do {
                    } while (list.remove(obj));
                }
            }
        }
    }

    /**
     * Puts an object into the cache and maps it to the key specified.
     * If mapKey corresnds to SelectAll object, the object gets mapped to
     * selection of all objects.
     */
    public void putMappedObject(Object mapKey, SLDataObject dataObject) throws Exception {
        if (mapKey == SelectAll.SELECT_ALL) {
            putObject(dataObject);
        } else {
            loadMappedObjects(mapKey);
            Object mapObj = mappings.get(mapKey);
            List list;
            if (mapObj instanceof List) {
                list = (List) mapObj;
            } else {
                mappings.put(mapKey, list = new ArrayList());
            }
            Object objKey = dataObject.hashKey();
            Object obj = objects.get(objKey);
            if (obj == null) {
                putObject(dataObject);
                list.add(dataObject);
            } else if (!list.contains(obj)) {
                list.add(obj);
            } else {
                Util.error("ERROR! StructuredCache: Attempt to reassign already mapped object.");
            }
        }
    }

    /**
     * Removes an object from the mapping list. If mapKey corresponds to
     * SelectAll object, the object gets removed from "Select All" selection
     * and from all mappings.
     */
    public void removeMappedObject(Object mapKey, SLDataObject dataObject) throws Exception {
        if (mapKey == SelectAll.SELECT_ALL) {
            removeObject(dataObject);
        } else {
            loadMappedObjects(mapKey);
            Object obj = objects.get(dataObject.hashKey());
            if (obj != null) {
                Object mapObj = mappings.get(mapKey);
                if (mapObj instanceof List) {
                    List list = (List) mapObj;
                    do {
                    } while (list.remove(obj));
                }
            }
        }
    }

    /**
     * Returns list of objects mapped to the key specified.
     */
    public List getMappedObjects(Object mapKey) throws Exception {
        if (mapKey == SelectAll.SELECT_ALL) {
            return getAllObjects();
        } else {
            loadMappedObjects(mapKey);
            Object mapObj = mappings.get(mapKey);
            if (mapObj instanceof List) {
                return Collections.unmodifiableList((List) mapObj);
            }
        }
        return Collections.EMPTY_LIST;
    }

    /**
     * Resorts objects in the selection mapped to mapKey.
     */
    public void sortObjects(Object mapKey, Comparator comp) throws Exception {
        if (mapKey == SelectAll.SELECT_ALL) {
            throw new Exception("Sorting of the whole selection is not implemented yet.");
        } else {
            loadMappedObjects(mapKey);
            Object mapObj = mappings.get(mapKey);
            if (mapObj instanceof List) {
                Collections.sort((List) mapObj, comp);
            }
        }
    }

    /**
     * Override this method to implement lazy materialization of
     * chached objects. Basically in this method you have to execute
     * query and fetch the data from persistent storage. If no objects
     * exist Nothing should be returned.
     */
    protected Object lookupAllObjects() throws Exception {
        return new Nothing();
    }

    /**
     * Override this method to implement lazy materialization of
     * linked chached objects. Basically in this method you have to execute
     * query and fetch the data from persistent storage. If select returns no
     * data, this method should return Nothing.
     */
    protected Object lookupMappedObjects(Object mapKey) throws Exception {
        return new Nothing();
    }

    /**
     * Initialization handler called by the constructor. This method is usefull
     * in case of anonymus inner class.
     */
    protected void init() {
    }

    /**
     * Use this class in lookupAllObjects() and lookupMappedObjects() to
     * signal that data select returned no values.
     */
    protected static class Nothing {
    }
}
